package com.audition.thread.day01.introduce;

/**
 * @author xukang
 * @date 2021-03-09 09:57
 */
public class IntroduceDemo {
    /**
     * 拒绝策略
     *
     * 第一种拒绝策略是 AbortPolicy，这种拒绝策略在拒绝任务时，
     * 会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException，
     * 让你感知到任务被拒绝了，于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
     * 第二种拒绝策略是 DiscardPolicy，这种拒绝策略正如它的名字所描述的一样，
     * 当新任务被提交后直接被丢弃掉，也不会给你任何的通知，相对而言存在一定的风险，
     * 因为我们提交的时候根本不知道这个任务会被丢弃，可能造成数据丢失。
     * 第三种拒绝策略是 DiscardOldestPolicy，如果线程池没被关闭且没有能力执行，
     * 则会丢弃任务队列中的头结点，通常是存活时间最长的任务，
     * 这种策略与第二种不同之处在于它丢弃的不是最新提交的，而是队列中存活时间最长的，
     * 这样就可以腾出空间给新提交的任务，但同理它也存在一定的数据丢失风险。
     * 第四种拒绝策略是 CallerRunsPolicy，相对而言它就比较完善了，当有新任务提交后，
     * 如果线程池没被关闭且没有能力执行，则把这个任务交于提交任务的线程执行，也就是谁提交任务，
     * 谁就负责执行任务。这样做主要有两点好处。
     * 第一点新提交的任务不会被丢弃，这样也就不会造成业务损失。
     * 第二点好处是，由于谁提交任务谁就要负责执行任务，这样提交任务的线程就得负责执行任务，而执行任务又是比较耗时的，在这段期间，提交任务的线程被占用，也就不会再提交新的任务，减缓了任务提交的速度，相当于是一个负反馈。在此期间，线程池中的线程也可以充分利用这段时间来执行掉一部分任务，腾出一定的空间，相当于是给了线程池一定的缓冲期。
     */
    /**
     * 线程池内部结构
     *线程池的内部结构主要由四部分组成，如图所示。
     *
     *  第一部分是线程池管理器，它主要负责管理线程池的创建、销毁、添加任务等管理操作，
     *  它是整个线程池的管家。
     *  第二部分是工作线程，也就是图中的线程 t0~t9，这些线程勤勤恳恳地从任务队列中获取任务并执行。
     *  第三部分是任务队列，作为一种缓冲机制，线程池会把当下没有处理的任务放入任务队列中，
     *  由于多线程同时从任务队列中获取任务是并发场景，此时就需要任务队列满足线程安全的要求，
     *  所以线程池中任务队列采用 BlockingQueue 来保障线程安全。
     *  第四部分是任务，任务要求实现统一的接口，以便工作线程可以处理和执行。
     */
    /**
     * 各种线程池使用的队列
     * LinkedBlockingQueue
     * 对于 FixedThreadPool 和 SingleThreadExector 而言，
     * 它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue，可以认为是无界队列。
     * 由于 FixedThreadPool 线程池的线程数是固定的，所以没有办法增加特别多的线程来处理任务，
     * 这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。这里需要注意，
     * 由于线程池的任务队列永远不会放满，所以线程池只会创建核心线程数量的线程，
     * 所以此时的最大线程数对线程池来说没有意义，因为并不会触发生成多于核心线程数的线程。
     *
     * SynchronousQueue
     * 第二种阻塞队列是 SynchronousQueue，对应的线程池是 CachedThreadPool。
     * 线程池 CachedThreadPool 的最大线程数是 Integer 的最大值，可以理解为线程数是可以无限扩展的。
     * CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反，
     * FixedThreadPool 的情况是阻塞队列的容量是无限的，而这里 CachedThreadPool 是线程数可以无限扩展，
     * 所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务，
     * 因为一旦有任务被提交就直接转发给线程或者创建新线程来执行，而不需要另外保存它们。
     *
     * 我们自己创建使用 SynchronousQueue 的线程池时，如果不希望任务被拒绝，
     * 那么就需要注意设置最大线程数要尽可能大一些，以免发生任务数大于最大线程数时，
     * 没办法把任务放到队列中也没有足够线程来执行任务的情况。
     *
     * DelayedWorkQueue
     * 第三种阻塞队列是DelayedWorkQueue，
     * 它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor，
     * 这两种线程池的最大特点就是可以延迟执行任务，比如说一定时间后执行任务或是每隔一定的时间执行一次任务。
     * DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序，而是会按照延迟的时间长短对任务进行排序，
     * 内部采用的是“堆”的数据结构。
     * 之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue，
     * 是因为它们本身正是基于时间执行任务的，而延迟队列正好可以把任务按时间进行排序，方便任务的执行。
     */
    /**
     * ForkJoinPool用的是哪一种？它自己及fork出来的子线程的队列都没有上限吗？   讲师回复：
     *             用的是ForkJoinPoo的内部类WorkQueue，有上限，上限在源码中规定了：
     * static final int MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M
     */
    /**
     * 综上所述我们就可以得出一个结论：
     *
     *  线程的平均工作时间所占比例越高，就需要越少的线程；
     *  线程的平均等待时间所占比例越高，就需要越多的线程；
     *  针对不同的程序，进行对应的实际测试就可以得到最合适的选择
     */
    /**
     * CPU 密集型任务
     * 首先，我们来看 CPU 密集型任务，比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍，如果设置过多的线程数，实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍以上，因为计算任务非常重，会占用大量的 CPU 资源，所以这时 CPU 的每个核心工作基本都是满负荷的，而我们又设置了过多的线程，每个线程都想去利用 CPU 资源来执行自己的任务，这就会造成不必要的上下文切换，此时线程数的增多并没有让性能提升，反而由于线程数量过多会导致性能下降。
     *
     * 针对这种情况，我们最好还要同时考虑在同一台机器上还有哪些其他会占用过多 CPU 资源的程序在运行，然后对资源使用做整体的平衡。
     *
     * 耗时 IO 型任务
     * 第二种任务是耗时 IO 型，比如数据库、文件的读写，网络通信等任务，这种任务的特点是并不会特别消耗 CPU 资源，但是 IO 操作很耗时，总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍，因为 IO 读写速度相比于 CPU 的速度而言是比较慢的，如果我们设置过少的线程数，就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数，那么当一部分线程正在等待 IO 的时候，它们此时并不需要 CPU 来计算，那么另外的线程便可以利用 CPU 去执行其他的任务，互不影响，这样的话在任务队列中等待的任务就会减少，可以更好地利用资源。
     *
     * 《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法：
     * 线程数 = CPU 核心数 *（1+平均等待时间/平均工作时间）
     * 通过这个公式，我们可以计算出一个合理的线程数量，如果任务的平均等待时间长，线程数就随之增加，而如果平均工作时间长，也就是对于我们上面的 CPU 密集型任务，线程数就随之减少。
     *
     * 太少的线程数会使得程序整体性能降低，而过多的线程也会消耗内存等其他资源，所以如果想要更准确的话，可以进行压测，监控 JVM 的线程情况以及 CPU 的负载情况，根据实际情况衡量应该创建的线程数，合理并充分利用资源。
     */
    /**
     * shutdown()
     * 第一种方法叫作 shutdown()，它可以安全地关闭一个线程池，调用 shutdown()
     * 方法之后线程池并不是立刻就被关闭，因为这时线程池中可能还有很多任务正在被执行，
     * 或是任务队列中有大量正在等待被执行的任务，
     * 调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。
     * 但这并不代表 shutdown() 操作是没有任何效果的，调用 shutdown() 方法后如果还有新的任务被提交，
     * 线程池则会根据拒绝策略直接拒绝后续新提交的任务。
     * isShutdown()
     * 第二个方法叫作 isShutdown()，它可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作，
     * 也就是是否执行了 shutdown 或者 shutdownNow 方法。这里需要注意，
     * 如果调用 isShutdown() 方法的返回的结果为 true 并不代表线程池此时已经彻底关闭了，
     * 这仅仅代表线程池开始了关闭的流程，也就是说，此时可能线程池中依然有线程在执行任务，
     * 队列里也可能有等待被执行的任务。
     * isTerminated()
     * 第三种方法叫作 isTerminated()，这个方法可以检测线程池是否真正“终结”了，这不仅代表线程池已关闭，
     * 同时代表线程池中的所有任务都已经都执行完毕了，因为我们刚才说过，
     * 调用 shutdown 方法之后，线程池会继续执行里面未完成的任务，不仅包括线程正在执行的任务，
     * 还包括正在任务队列中等待的任务。比如此时已经调用了 shutdown 方法，但是有一个线程依然在执行任务，
     * 那么此时调用 isShutdown 方法返回的是 true ，而调用 isTerminated 方法返回的便是 false ，
     * 因为线程池中还有任务正在在被执行，线程池并没有真正“终结”。直到所有任务都执行完毕了，
     * 调用 isTerminated() 方法才会返回 true，这表示线程池已关闭并且线程池内部是空的，
     * 所有剩余的任务都执行完毕了。
     * awaitTermination()
     * 第四个方法叫作 awaitTermination()，它本身并不是用来关闭线程池的，而是主要用来判断线程池状态的。
     * 比如我们给 awaitTermination 方法传入的参数是 10 秒，那么它就会陷入 10 秒钟的等待，
     * 直到发生以下三种情况之一：
     *
     * 等待期间（包括进入等待状态之前）线程池已关闭并且所有已提交的任务（包括正在执行的和队列中等待的）都执行完毕，
     * 相当于线程池已经“终结”了，方法便会返回 true；
     * 等待超时时间到后，第一种线程池“终结”的情况始终未发生，方法返回 false；
     * 等待期间线程被中断，方法会抛出 InterruptedException 异常。
     *
     * 也就是说，调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间，如果在等待时间内，
     * 线程池已关闭并且内部的任务都执行完毕了，也就是说线程池真正“终结”了，那么方法就返回 true，
     * 否则超时返回 fasle。
     * 我们则可以根据 awaitTermination() 返回的布尔值来判断下一步应该执行的操作。
     * shutdownNow()
     * 最后一个方法是 shutdownNow()，也是 5 种方法里功能最强大的，
     * 它与第一种 shutdown 方法不同之处在于名字中多了一个单词 Now，也就是表示立刻关闭的意思。
     * 在执行 shutdownNow 方法之后，首先会给所有线程池中的线程发送 interrupt 中断信号，
     * 尝试中断这些任务的执行，然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回，
     * 我们可以根据返回的任务 List 来进行一些补救的操作，例如记录在案并在后期重试。shutdownNow() 的源码如下所示。
     * 复制代码public List<Runnable> shutdownNow() {
     *     List<Runnable> tasks;
     *     final ReentrantLock mainLock = this.mainLock;
     *     mainLock.lock();
     *
     *     try {
     *         checkShutdownAccess();
     *         advanceRunState(STOP);
     *         interruptWorkers();
     *         tasks = drainQueue();
     *     } finally {
     *         mainLock.unlock();
     *     }
     *
     *     tryTerminate();
     *     return tasks;
     *  }
     *
     * 你可以看到源码中有一行 interruptWorkers() 代码，这行代码会让每一个已经启动的线程都中断，
     * 这样线程就可以在执行任务期间检测到中断信号并进行相应的处理，提前结束任务。这里需要注意的是，
     * 由于 Java 中不推荐强行停止线程的机制的限制，即便我们调用了 shutdownNow 方法，
     * 如果被中断的线程对于中断信号不理不睬，那么依然有可能导致任务不会停止。
     * 可见我们在开发中落地最佳实践是很重要的，我们自己编写的线程应当具有响应中断信号的能力，
     * 正确停止线程的方法在第 2 讲有讲过，应当利用中断信号来协同工作。
     * 在掌握了这 5 种关闭线程池相关的方法之后，我们就可以根据自己的业务需要，选择合适的方法来停止线程池，
     * 比如通常我们可以用 shutdown() 方法来关闭，这样可以让已提交的任务都执行完毕，但是如果情况紧急，
     * 那我们就可以用 shutdownNow 方法来加快线程池“终结”的速度。
     */
    /**
     * 线程复用原理
     * 我们知道线程池会使用固定数量或可变数量的线程来执行任务，但无论是固定数量或可变数量的线程，
     * 其线程数量都远远小于任务数量，面对这种情况线程池可以通过线程复用让同一个线程去执行不同的任务，
     * 那么线程复用背后的原理是什么呢？
     * 线程池可以把线程和任务进行解耦，线程归线程，任务归任务，
     * 摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。在线程池中，
     * 同一个线程可以从 BlockingQueue 中不断提取新任务来执行，其核心原理在于线程池对 Thread 进行了封装，
     * 并不是每次执行任务都会调用 Thread.start() 来创建新线程，而是让每个线程去执行一个“循环任务”，
     * 在这个“循环任务”中，不停地检查是否还有任务等待被执行，如果有则直接去执行这个任务，
     * 也就是调用任务的 run 方法，把 run 方法当作和普通方法一样的地位去调用，
     * 相当于把每个任务的 run() 方法串联了起来，所以线程数量并不增加。
     * 我们首先来复习一下线程池创建新线程的时机和规则：
     *
     * 如流程图所示，当提交任务后，线程池首先会检查当前线程数，如果此时线程数小于核心线程数，
     * 比如最开始线程数量为 0，则新建线程并执行任务，随着任务的不断增加，线程数会逐渐增加并达到核心线程数，
     * 此时如果仍有任务被不断提交，就会被放入 workQueue 任务队列中，
     * 等待核心线程执行完当前任务后重新从 workQueue 中提取正在等待被执行的任务。此时，
     * 假设我们的任务特别的多，已经达到了 workQueue 的容量上限，这时线程池就会启动后备力量，
     * 也就是 maxPoolSize 最大线程数，线程池会在 corePoolSize 核心线程数的基础上继续创建线程来执行任务，
     * 假设任务被不断提交，线程池会持续创建线程直到线程数达到 maxPoolSize 最大线程数，
     * 如果依然有任务被提交，这就超过了线程池的最大处理能力，这个时候线程池就会拒绝这些任务，
     * 我们可以看到实际上任务进来之后，线程池会逐一判断 corePoolSize 、workQueue 、maxPoolSize ，
     * 如果依然不能满足需求，则会拒绝任务。
     * 我们接下来具体看看代码是如何实现的，我们从 execute 方法开始分析，源码如下所示。
     * 复制代码public void execute(Runnable command) {
     *     if (command == null)
     *         throw new NullPointerException();
     *     int c = ctl.get();
     *     if (workerCountOf(c) < corePoolSize) {
     *         if (addWorker(command, true))
     *             return;
     *         c = ctl.get();
     *     }
     *     if (isRunning(c) && workQueue.offer(command)) {
     *         int recheck = ctl.get();
     *         if (! isRunning(recheck) && remove(command))
     *             reject(command);
     *         else if (workerCountOf(recheck) == 0)
     *             addWorker(null, false);
     *     }
     *     else if (!addWorker(command, false))
     *         reject(command);
     * }
     *
     * 线程复用源码解析
     * 这段代码短小精悍，内容丰富，接下来我们具体分析代码中的逻辑，首先看下前几行：
     * 复制代码//如果传入的Runnable的空，就抛出异常
     * if (command == null)
     *     throw new NullPointerException();
     *
     * execute 方法中通过 if 语句判断 command ，也就是  Runnable 任务是否等于 null，
     * 如果为 null 就抛出异常。
     * 接下来判断当前线程数是否小于核心线程数，如果小于核心线程数就调用 addWorker() 方法增加一个 Worker，
     * 这里的 Worker 就可以理解为一个线程：
     * 复制代码if (workerCountOf(c) < corePoolSize) {
     *     if (addWorker(command, true))
     *         return;
     *         c = ctl.get();
     * }
     *
     * 那 addWorker 方法又是做什么用的呢？
     * addWorker 方法的主要作用是在线程池中创建一个线程并执行第一个参数传入的任务，
     * 它的第二个参数是个布尔值，如果布尔值传入 true 代表增加线程时判断当前线程是否少于 corePoolSize，
     * 小于则增加新线程，大于等于则不增加；同理，
     * 如果传入 false 代表增加线程时判断当前线程是否少于 maxPoolSize，小于则增加新线程，大于等于则不增加，
     * 所以这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增线程的判断。
     * addWorker() 方法如果返回 true 代表添加成功，如果返回 false 代表添加失败。
     * 我们接下来看下一部分代码：
     * 复制代码if (isRunning(c) && workQueue.offer(command)) {
     *     int recheck = ctl.get();
     *     if (! isRunning(recheck) && remove(command))
     *         reject(command);
     *     else if (workerCountOf(recheck) == 0)
     *         addWorker(null, false);
     * }
     *
     * 如果代码执行到这里，说明当前线程数大于或等于核心线程数或者 addWorker 失败了，
     * 那么就需要通过 if (isRunning(c) && workQueue.offer(command)) 检查线程池状态是否为 Running，
     * 如果线程池状态是 Running 就把任务放入任务队列中，也就是 workQueue.offer(command)。
     * 如果线程池已经不处于 Running 状态，说明线程池被关闭，那么就移除刚刚添加到任务队列中的任务，
     * 并执行拒绝策略，代码如下所示：
     * 复制代码if (! isRunning(recheck) && remove(command))
     *     reject(command);
     *
     * 下面我们再来看后一个 else 分支：
     * 复制代码else if (workerCountOf(recheck) == 0)
     *     addWorker(null, false);
     *
     * 能进入这个 else 说明前面判断到线程池状态为 Running，
     * 那么当任务被添加进来之后就需要防止没有可执行线程的情况发生（比如之前的线程被回收了或意外终止了），
     * 所以此时如果检查当前线程数为 0，也就是 workerCountOf**(recheck) == 0，那就执行 addWorker() 方法新建线程。
     * 我们再来看最后一部分代码：
     * 复制代码else if (!addWorker(command, false))
     *     reject(command);
     *
     * 执行到这里，说明线程池不是 Running 状态或线程数大于或等于核心线程数并且任务队列已经满了，
     * 根据规则，此时需要添加新线程，直到线程数达到“最大线程数”，
     * 所以此时就会再次调用 addWorker 方法并将第二个参数传入 false，
     * 传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize，小于则增加新线程，大于等于则不增加，
     * 也就是以 maxPoolSize 为上限创建新的 worker；addWorker 方法如果返回 true 代表添加成功，
     * 如果返回 false 代表任务添加失败，说明当前线程数已经达到 maxPoolSize，
     * 然后执行拒绝策略 reject 方法。如果执行到这里线程池的状态不是 Running，
     * 那么 addWorker 会失败并返回 false，所以也会执行拒绝策略 reject 方法。
     * 可以看出，在 execute 方法中，多次调用 addWorker 方法把任务传入，
     * addWorker 方法会添加并启动一个 Worker，这里的 Worker 可以理解为是对 Thread 的包装
     * ，Worker 内部有一个 Thread 对象，它正是最终真正执行任务的线程，
     * 所以一个 Worker 就对应线程池中的一个线程，addWorker 就代表增加线程。
     * 线程复用的逻辑实现主要在 Worker 类中的 run 方法里执行的 runWorker 方法中，
     * 简化后的 runWorker 方法代码如下所示。
     * 复制代码runWorker(Worker w) {
     *     Runnable task = w.firstTask;
     *     while (task != null || (task = getTask()) != null) {
     *         try {
     *             task.run();
     *         } finally {
     *             task = null;
     *         }
     *     }
     * }
     *
     * 可以看出，实现线程复用的逻辑主要在一个不停循环的 while 循环体中。
     *
     * 通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。
     * 直接调用 task 的 run 方法来执行具体的任务（而不是新建线程）。
     *
     * 在这里，我们找到了最终的实现，通过取 Worker 的 firstTask
     * 或者 getTask方法从 workQueue 中取出了新任务，并直接调用 Runnable 的 run 方法来执行任务，
     * 也就是如之前所说的，每个线程都始终在一个大循环中，反复获取任务，然后执行任务，从而实现了线程的复用。
     */
    /**
     * 锁的 7 大分类
     * 需要首先指出的是，这些多种多样的分类，是评价一个事物的多种标准，比如评价一个城市，
     * 标准有人口多少、经济发达与否、城市面积大小等。而一个城市可能同时占据多个标准，以北京而言，
     * 人口多，经济发达，同时城市面积还很大。
     *
     * 同理，对于 Java 中的锁而言，一把锁也有可能同时占有多个标准，符合多种分类，
     * 比如 ReentrantLock 既是可中断锁，又是可重入锁。
     *
     * 根据分类标准我们把锁分为以下 7 大类别，分别是：
     *
     *  偏向锁/轻量级锁/重量级锁；
     *  可重入锁/非可重入锁；
     *  共享锁/独占锁；
     *  公平锁/非公平锁；
     *  悲观锁/乐观锁；
     *  自旋锁/非自旋锁；
     *  可中断锁/不可中断锁。
     *
     * 以上是常见的分类标准，下面我们来逐一介绍它们的含义。
     * 偏向锁/轻量级锁/重量级锁
     * 第一种分类是偏向锁/轻量级锁/重量级锁，这三种锁特指 synchronized 锁的状态，
     * 通过在对象头中的 mark word 来表明锁的状态。
     *
     *  偏向锁
     *
     * 如果自始至终，对于这把锁都不存在竞争，那么其实就没必要上锁，只需要打个标记就行了，
     * 这就是偏向锁的思想。一个对象被初始化后，还没有任何线程来获取它的锁时，那么它就是可偏向的，
     * 当有第一个线程来访问它并尝试获取锁的时候，它就将这个线程记录下来，
     * 以后如果尝试获取锁的线程正是偏向锁的拥有者，就可以直接获得锁，开销很小，性能最好。
     *
     *  轻量级锁
     *
     * JVM 开发者发现在很多情况下，synchronized 中的代码是被多个线程交替执行的，而不是同时执行的，
     * 也就是说并不存在实际的竞争，或者是只有短时间的锁竞争，用 CAS 就可以解决，这种情况下，
     * 用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候，被另一个线程访问，
     * 说明存在竞争，那么偏向锁就会升级为轻量级锁，线程会通过自旋的形式尝试获取锁，而不会陷入阻塞。
     *
     *  重量级锁
     *
     * 重量级锁是互斥锁，它是利用操作系统的同步机制实现的，所以开销相对比较大。当多个线程直接有实际竞争，
     * 且锁竞争时间长的时候，轻量级锁不能满足需求，锁就会膨胀为重量级锁。
     * 重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
     *
     * 你可以发现锁升级的路径：无锁→偏向锁→轻量级锁→重量级锁。
     *
     * 综上所述，偏向锁性能最好，可以避免执行 CAS 操作。
     * 而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒，性能中等。
     * 重量级锁则会把获取不到锁的线程阻塞，性能最差。
     * 可重入锁/非可重入锁
     * 第 2 个分类是可重入锁和非可重入锁。可重入锁指的是线程当前已经持有这把锁了，
     * 能在不释放这把锁的情况下，再次获取这把锁。同理，不可重入锁指的是虽然线程当前持有了这把锁，
     * 但是如果想再次获取这把锁，也必须要先释放锁后才能再次尝试获取。
     *
     * 对于可重入锁而言，最典型的就是 ReentrantLock 了，正如它的名字一样，reentrant 的意思就是可重入
     * ，它也是 Lock 接口最主要的一个实现类。
     * 共享锁/独占锁
     * 第 3 种分类标准是共享锁和独占锁。共享锁指的是我们同一把锁可以被多个线程同时获得，而独占锁指的就是，
     * 这把锁只能同时被一个线程获得。我们的读写锁，就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁，
     * 是共享锁，而写锁是独占锁。读锁可以被同时读，可以同时被多个线程持有，而写锁最多只能同时被一个线程持有。
     * 公平锁/非公平锁
     * 第 4 种分类是公平锁和非公平锁。公平锁的公平的含义在于如果线程现在拿不到这把锁，
     * 那么线程就都会进入等待，开始排队，在等待队列里等待时间长的线程会优先拿到这把锁，有先来先得的意思。
     * 而非公平锁就不那么“完美”了，它会在一定情况下，忽略掉已经在排队的线程，发生插队现象。
     * 悲观锁/乐观锁
     * 第 5 种分类是悲观锁，以及与它对应的乐观锁。悲观锁的概念是在获取资源之前，必须先拿到锁，
     * 以便达到“独占”的状态，当前线程在操作资源的时候，其他线程由于不能拿到锁，所以其他线程不能来影响我。
     * 而乐观锁恰恰相反，它并不要求在获取资源前拿到锁，也不会锁住资源；相反，乐观锁利用 CAS 理念，
     * 在不独占资源的情况下，完成了对资源的修改。
     * 自旋锁/非自旋锁
     * 第 6 种分类是自旋锁与非自旋锁。自旋锁的理念是如果线程现在拿不到锁，
     * 并不直接陷入阻塞或者释放 CPU 资源，而是开始利用循环，不停地尝试获取锁，
     * 这个循环过程被形象地比喻为“自旋”，就像是线程在“自我旋转”。相反，非自旋锁的理念就是没有自旋的过程，
     * 如果拿不到锁就直接放弃，或者进行其他的处理逻辑，例如去排队、陷入阻塞等。
     * 可中断锁/不可中断锁
     * 第 7 种分类是可中断锁和不可中断锁。在 Java 中，synchronized 关键字修饰的锁代表的是不可中断锁，
     * 一旦线程申请了锁，就没有回头路了，只能等到拿到锁以后才能进行其他的逻辑处理。
     * 而我们的 ReentrantLock 是一种典型的可中断锁，例如使用 lockInterruptibly 方法在获取锁的过程中，
     * 突然不想获取了，那么也可以在中断之后去做其他的事情，不需要一直傻等到获取到锁才离开。
     *
     * 好了，本课时的内容就全部讲完了，下一课时我将会从悲观锁和乐观锁开始详细讲解这一系列的锁的具体概念和特点，下一课时见。
     */
}

